package com.geek.gkfood.framework.redis.locker;

import com.geek.gkfood.util.excp.GkFoodCodeEnum;
import com.geek.gkfood.util.excp.GkFoodException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisClusterConfiguration;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisNode;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisCluster;
import redis.clients.jedis.Protocol;
import redis.clients.jedis.exceptions.JedisNoScriptException;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Set;

/**
 * <h1>Redis Lua 锁工具类。</h1><br/>
 *
 * <h1><br/><a href="https://u.geekbang.org/subject/arch3/100614201">Java 业务架构实战营：https://u.geekbang.org/subject/arch3/100614201</a></h1><br/><h1><a href="https://gitee.com/ylimhhmily/BizArchPracticeCamp">Java 业务架构实战营代码地址：https://gitee.com/ylimhhmily/BizArchPracticeCamp</a></h1><br/><h1><a href="https://u.geekbang.org/subject/java4th/1001148?source=app_share">高级 Java 工程体系课 2023 版：https://u.geekbang.org/subject/java4th/1001148?source=app_share</a></h1><br/><h1><a href="https://time.geekbang.org/column/intro/100312101?tab=catalog">Dubbo 源码剖析与实战：https://time.geekbang.org/column/intro/100312101?tab=catalog</a></h1>
 *
 * @author hmilyylimh
 * ^_^
 * @version 0.0.1
 * ^_^
 * @date 2023/9/24 06:10
 */
@Slf4j
public abstract class RedisLuaLocker {

    private static final Long SUCC_FLAG = 1L;

    @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    private RedisClusterConfiguration redisClusterConfiguration;

    /** <h2>默认集群中节点数量</h2> **/
    public static final int DEFAULT_CLUSTER_NODE_SIZE = 3;
    /** <h2>原始的 lua 脚本内容</h2> **/
    private volatile String luaRawContent;
    /** <h2>将 luaRawContent 送到 redis 后台，就可以拿到对应的哈希标识</h2> **/
    private volatile String cacheScriptSha = null;
    /** <h2>假如redis本地保存的还是个旧的话，若继续发起对redis的写请求的话，则异常信息中会包含这个关键字</h2> **/
    private static final String NOSCRIPT_FLAG = "NOSCRIPT";

    @EventListener(ContextRefreshedEvent.class)
    public void refresh() throws IOException {
        luaRawContent = IOUtils.toString(
                IOUtils.toByteArray(RedisLuaLocker.class.getResourceAsStream(getResourcePath())),
                Protocol.CHARSET);
        log.info("loadLuaRawContent={}", luaRawContent);
    }

    public abstract String getResourcePath();

    /**
     * <h2>尝试执行加锁执行 lua 脚本操作。</h2>
     *
     * @throws
     * @param: keys
     * @param: args
     * @return: boolean
     * @author hmilyylimh
     * @date: 2023/9/24
     */
    public boolean tryLock(ArrayList<String> keys, ArrayList<String> args) {
        boolean pass = false;
        try {
            pass = execLua(keys, args);
        } catch (Throwable e) {
            log.error("tryLock error.", e);
            throw new GkFoodException(GkFoodCodeEnum.LIMIT_EXCEPTION, e);
        }

        return pass;
    }

    /**
     * <h2>执行 lua 脚本。</h2>
     *
     * @throws
     * @param: keys
     * @param: args
     * @return: boolean，{@code true}: 通过，可以正常执行业务逻辑; {@code false}: 不通过，已控制恶意行为;
     * @author hmilyylimh
     * @date: 2023/9/24
     */
    private boolean execLua(ArrayList<String> keys, ArrayList<String> args) {
        Long result = null;
        String firstRedisKey = keys.get(0);
        try {
            String scriptSha = getScirptShaFromCache(firstRedisKey, luaRawContent);
            if (StringUtils.isBlank(scriptSha)) {
                throw new GkFoodException(GkFoodCodeEnum.REDIS_LUA_EXCEPTION, "firstRedisKey=" + firstRedisKey);
            }

            // 执行 redis lua 脚本内容
            result = execLuaInner(keys, args, scriptSha);
        } catch (DataAccessException | JedisNoScriptException dae ) {
            log.error("execLua error.", dae);
            String message = dae.getMessage();
            if (message != null && message.toUpperCase().contains(NOSCRIPT_FLAG)) {
                try {
                    // 异常发现 redis 的 sha 值不在 redis 后台的 map 中的话，则删除了缓存重新再获取，然后重新再调用一次业务方法
                    cacheScriptSha = null;
                    String newScriptSha = getScirptShaFromCache(firstRedisKey, luaRawContent);
                    result = execLuaInner(keys, args, newScriptSha);
                } catch (Throwable e) {
                    throw e;
                }
            } else {
                throw dae;
            }
        } catch (Throwable e) {
            log.error("firstRedisKey = {} , execLua fail, reject invoke", firstRedisKey, e);
            throw e;
        }

        boolean succ = SUCC_FLAG.equals(result);
        log.info("firstRedisKey = {} , execLua.result = {} -- {}", firstRedisKey, result, (succ ? "succ":"fail"));
        return succ;
    }

    private Long execLuaInner(ArrayList<String> keys, ArrayList<String> args, String scriptSha) {
        Long result = (Long) redisTemplate.execute(new RedisCallback<Long>() {
            public Long doInRedis(RedisConnection connection) throws DataAccessException {
                Object nativeConnection = connection.getNativeConnection();
                // JedisCluster 集群
                if (nativeConnection instanceof JedisCluster) {
                    return (Long) ((JedisCluster) nativeConnection).evalsha(scriptSha, keys, args);
                }
                // Jedis 单点
                else if (nativeConnection instanceof Jedis) {
                    return (Long) ((Jedis) nativeConnection).evalsha(scriptSha, keys, args);
                }
                return null;
            }
        });
        return result;
    }

    private String getScirptShaFromCache(String redisKey, String luaContent) {
        if (cacheScriptSha == null) {
            synchronized (this) {
                if (cacheScriptSha == null) {
                    cacheScriptSha = getScirptShaFromRedis(redisKey, luaContent);
                }
            }
        }

        return cacheScriptSha;
    }

    private String getScirptShaFromRedis(String redisKey, String luaContent) {
        String scriptSha = (String) redisTemplate.execute(new RedisCallback<String>() {
            public String doInRedis(RedisConnection connection) throws DataAccessException {
                Object nativeConnection = connection.getNativeConnection();
                // JedisCluster 集群
                if (nativeConnection instanceof JedisCluster) {
                    return ((JedisCluster) nativeConnection).scriptLoad(luaContent, redisKey);
                }
                // Jedis 单点
                else if (nativeConnection instanceof Jedis) {
                    return ((Jedis) nativeConnection).scriptLoad(luaContent);
                }
                return null;
            }
        });

        log.info("scriptSha={}", scriptSha);
        return scriptSha;
    }

    /**
     * <h2>分片组合key，主要目的是将不同的 redisKey 打散到不同的 redis 节点上，防止单点问题。</h2>
     *
     * @throws
     * @param: rawKey
     * @return: int
     * @author hmilyylimh
     * @date: 2023/9/24
     */
    protected int getShardingHash(String rawKey) {
        Set<RedisNode> clusterNodes = redisClusterConfiguration.getClusterNodes();
        int shardingCount = DEFAULT_CLUSTER_NODE_SIZE;
        if (clusterNodes != null && !clusterNodes.isEmpty()) {
            shardingCount = clusterNodes.size();
        }
        String suffix = rawKey;
        int hashCode = suffix.hashCode();
        // 高低异或避免 hash 后的数据倾斜
        hashCode = (hashCode) ^ (hashCode >>> 16);
        int module = 0;
        // 是否是 2 的幂值
        if ((shardingCount & -shardingCount) == shardingCount) {
            module = hashCode & shardingCount - 1;
        } else {
            module = hashCode % shardingCount;
        }

        return module;
    }
}